/*
 * JSF Toolkit Component Framework
 * Copyright (C) 2007 Noah Sloan <iamnoah A-T gmail D0T com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 */
package com.jsftoolkit.base.renderer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import com.jsftoolkit.utils.Utils;
import com.jsftoolkit.utils.xmlpull.AttributeEvent;
import com.jsftoolkit.utils.xmlpull.BodyText;
import com.jsftoolkit.utils.xmlpull.PullEvent;
import com.jsftoolkit.utils.xmlpull.PullEventCollector;

/**
 * Extends a {@link PullEventCollector} to provide more rendering specific
 * events. Attributes and element text content matching the {@link #VAR_PATTERN}
 * will be replaced with {@link VarAttribEvent}s and {@link VarTextEvent}s.
 * 
 * @author noah
 * 
 */
public class RenderEventsCollector extends PullEventCollector {

	public static final Pattern VAR_PATTERN = Pattern.compile(
			"(^|[^\\\\])\\$\\{([^\\|&&\\S]+?)(\\|(([^\\}]|\\\\\\})*?))?\\}",
			Pattern.DOTALL);

	/**
	 * 
	 * @param text
	 *            the text to process.
	 * @throws IOException
	 * @throws SAXException
	 */
	public RenderEventsCollector(String text) throws IOException, SAXException {
		super(text);
		process();
	}

	/**
	 * 
	 * @param reader
	 *            the {@link XMLReader} instance to use.
	 * @param text
	 *            the text to parse.
	 * @throws IOException
	 * @throws SAXException
	 */
	public RenderEventsCollector(XMLReader reader, String text)
			throws IOException, SAXException {
		super(reader, text);
		process();
	}

	/**
	 * Process the events list, inserting/replacing events where appropriate due
	 * to variable substitutions.
	 */
	private void process() {
		ListIterator<PullEvent> lit = events.listIterator();
		while (lit.hasNext()) {
			PullEvent event = lit.next();
			if (event instanceof AttributeEvent) {
				AttributeEvent ae = (AttributeEvent) event;
				if ("decode".equalsIgnoreCase(ae.getName())) {
					lit.remove();
					lit.add(new DecodeEvent(ae.getValue()));
				} else if ("passThrough".equalsIgnoreCase(ae.getName())) {
					// determine which attributes should be passed through
					String value = Utils.toString(ae.getValue());
					Set<String> allowed = new HashSet<String>(
							HtmlRenderer.PASS_THROUGH);

					if (value.length() > 0 && value.charAt(0) == '!') {
						allowed.removeAll(Arrays.asList(value.substring(1)
								.split(",")));
					} else if (!Utils.isEmpty(value)) {
						allowed = new HashSet<String>(Arrays.asList(value
								.split(",")));
					}

					lit.remove();
					lit.add(new PassThrough(allowed));
				} else {
					VarAttribEvent vae = convertToVarAttribute(ae);
					if (vae != null) {
						lit.remove();
						lit.add(vae);
					}
				}
			} else if (event instanceof BodyText) {
				// replace the body text with a printf format string
				Matcher matcher = VAR_PATTERN.matcher(((BodyText) event)
						.getText().toString().replaceAll("%", "%%"));

				boolean removed = false;
				while (matcher.find()) {
					if (!removed) {
						lit.remove();
						removed = true;
					}
					StringBuffer sb = new StringBuffer();
					matcher.appendReplacement(sb, Utils.toString(matcher
							.group(1)));
					// the one body text event gets broken into multiple body
					// text and var text events
					lit.add(new BodyText(sb));
					lit.add(new VarTextEvent(matcher.group(2), matcher
							.groupCount() > 3 ? matcher.group(4) : null));
				}

				StringBuffer sb = new StringBuffer();
				matcher.appendTail(sb);
				if (removed) {
					lit.add(new BodyText(sb));
				}
			}
		}
	}

	/**
	 * Parses the given {@link AttributeEvent} value for variable substitutions/
	 * 
	 * @param ae
	 *            the attribute event to parse.
	 * @return null if there are no substitutions, otherwise a replacement
	 *         {@link VarAttribEvent}.
	 */
	public static VarAttribEvent convertToVarAttribute(AttributeEvent ae) {
		VarAttribEvent vae = null;

		// the attribute value may contain multiple attributes to
		// insert, so we create a printf pattern, and determine what
		// the attributes are.
		Matcher matcher = VAR_PATTERN.matcher(ae.getValue().replaceAll("%",
				"%%"));
		if (matcher.find()) {
			// found a var attribute, so remove the plain attribute
			// event

			List<String> props = new ArrayList<String>();
			List<String> defaults = new ArrayList<String>();
			StringBuffer pattern = new StringBuffer(ae.getValue().length());

			do {
				// replace variable occurrences with %s
				matcher.appendReplacement(pattern, matcher.group(1) + "%s");
				// save the attribute name and default
				props.add(matcher.group(2));
				defaults
						.add(matcher.groupCount() > 3 ? matcher.group(4) : null);
			} while (matcher.find());

			matcher.appendTail(pattern);

			vae = new VarAttribEvent(ae.getName(), pattern.toString(), props,
					defaults);
		}
		return vae;
	}
}
